今天我們針對schema,分享一些進階的概念。
Abstract constraint可以幫助我們自己定義想要的constraint
。例如:
abstract constraint must_contain_a() {
errmessage :=
'{__subject__} must contain at least one `a` or `A`.';
using (
contains(str_lower(__subject__), "a")
) ;
}
type User {
name: str {
constraint must_contain_a
}
}
這裡我們定義一個abstract constraint
must_contain_a
來確認其自身必須包含最少一個「"a"」或「"A"」字母。接著我們將must_contain_a
施加於User object
的name property
。比較特別的是可以在errmessage
中使用{__subject__}
來顯示自身,有點像是Python的f-string功能。
此時,我們試著insert
一個name property
為「"John"」的User object
:
select(insert User {name:="John"}) {name};
因為John內並沒有「"a"」或「"A"」字母,所以會報錯如下:
edgedb error: ConstraintViolationError: name must contain at least one `a` or `A`.
Detail: violated constraint 'default::must_contain_a' on property 'name' of object type 'default::User'
如果insert
name property
為「"May"」或「"MAY"」的User object
,則皆會成功:
with names:= {"May", "MAY"}
for name in names
union (
select (insert User{name:=name}) {name}
);
{default::User {name: 'May'}, default::User {name: 'MAY'}}
abstract link可以幫助我們建立抽象化的link
,使其可以作用在多個object type
。
考慮schema如下:
abstract link link_with_note {
note: str;
}
type Person {
name: str;
multi friends: Person {
extending link_with_note;
};
}
可以看出abstract link link_with_note
可以經由Person object
extending
後,作為Person object
中的multi friends link
;而abstract link link_with_note
中的note
則可以經由Person object
以link property
型式來存取。
此時我們insert
兩個name property
為「"John"」及「"Tom"」的Person object
,並以John
及Tom
來代稱:
with names:= {"John", "Tom"}
for name in names
union (
select(insert Person {name:=name}) {name}
);
{default::Person {name: 'John'}, default::Person {name: 'Tom'}}
假設John
是個常常記不清楚,朋友是在哪個階段認識的。此時他可以更新自己的multi friends link
中的note link property
來註記Tom
是自己的高中同學:
with john:= (select Person filter .name="John"),
tom:= (select Person filter .name="Tom")
update john
set {
friends:= tom {@note:= "high school classmate"}
};
可以使用下面query確認註記成功:
select Person {**};
{
default::Person {
id: 7cc79298-53f1-11ef-926f-7364dedf390e,
name: 'John',
friends: {
default::Person {
id: 7cc794f0-53f1-11ef-926f-d7166079d714,
name: 'Tom',
@note: 'high school classmate'},
},
},
default::Person {
id: 7cc794f0-53f1-11ef-926f-d7166079d714,
name: 'Tom',
friends: {}},
}
不知道大家有沒有感受到,link property
其實是個很有趣的功能呀?我自己是將link property
想像為link
的metadata
,可以用來存取一些額外的資訊,並經由object type
來存取。
最後提醒大家,官方文件中提到link property
只能是single
與optional
。
本小節取材自Easy EdgeDB第十七章第四個練習題。如何能夠在保存data的情況下,修改object type
的schema,將其中的property
抽取出來為獨立的abstract type
?
考慮schema如下:
type User {
email: str;
}
此時我們insert
一個User object
:
select (insert User {email:="John@example.com"}) {email};
{default::User {email: 'John@example.com'}}
此時我們可以將email property
抽取為HasEmail
,並改寫User object
來extending
HasEmail
:
abstract type HasEmail {
email: str;
}
type User extending HasEmail;
接著執行migration
:
did you create object type 'default::HasEmail'? [y,n,l,c,b,s,q,?]
> y
did you alter object type 'default::User'? [y,n,l,c,b,s,q,?]
> y
The following extra DDL statements will be applied:
ALTER TYPE default::User {
ALTER PROPERTY email {
DROP OWNED;
RESET TYPE;
};
};
(approved as part of an earlier prompt)
此時可以確認原先的User object
的確仍然在資料庫內:
select User {*};
{default::User {id: d41b9f02-5406-11ef-a0e8-bb18dd29e982, email: 'John@example.com'}}
這個抽取技巧有點類似Python的Mixin或是Rust的trait。
本小節取材自官方文件。
backlink
與multi link
即是EdgeDB中的many-to-one
及one-to-many
關係。
舉例來說,我們將針對下面這個情況,分別使用backlink
與multi link
兩種方式來寫寫看:
User object
可以擁有多件Shirt object
。Shirt object
只能被一個User object
所擁有。backlink
的想法是many-to-one
,可以想成是many Shirt object to one Person object
。
schema定義如下:
type Person {
required name: str
}
type Shirt {
required color: str;
owner: Person;
}
執行下面query,生成一個Person object
及三個Shirt object
:
insert Person {name:="John"};
with colors:= {"red", "green", "blue"}
for color in colors
union (
insert Shirt {
color:=color,
owner:= assert_single(Person)
}
);
此時,如果我們想由Person object
下手,取得其所擁有的Shirt object
時,可以寫為:
select Person {name, shirts:= .<owner[is Shirt]{color} };
{
default::Person {
name: 'John',
shirts: {
default::Shirt {color: 'red'},
default::Shirt {color: 'green'},
default::Shirt {color: 'blue'}
},
},
}
可以看出來shirts
原本是沒有定義在schema內,而是我們使用backlink
所取得的。
如果這樣的運算很常使用的話,也可以將其直接定義於schema內。例如:
type Person {
required name: str
shirts:= .<owner[is Shirt]
}
type Shirt {
required color: str;
owner: Person;
}
此時依然可以從Person object
中取得其所擁有的Shirt object
:
select Person {name, shirts: {color}};
{
default::Person {
name: 'John',
shirts: {
default::Shirt {color: 'red'},
default::Shirt {color: 'green'},
default::Shirt {color: 'blue'}
},
},
}
multi link
的想法是one-to-many
,可以想成是one Person object to many Shirt object
。
schema定義如下:
type Person {
required name: str;
multi shirts: Shirt {
# ensures a one-to-many relationship
constraint exclusive;
}
}
type Shirt {
required color: str;
}
其中的constraint exclusive
非常重要,可以確保一個Shirt object
只能被一個Person object
擁有。
執行下面query,生成一個Person object
及三個Shirt object
:
with colors:= {"red", "green", "blue"}
for color in colors
union (
insert Shirt {
color:=color,
}
);
insert Person {name:="John", shirts:= Shirt};
由Person object
中取得其所擁有的Shirt object
:
select Person {name, shirts: {color}};
{
default::Person {
name: 'John',
shirts: {
default::Shirt {color: 'red'},
default::Shirt {color: 'green'},
default::Shirt {color: 'blue'}
},
},
}
官方文件中建議當有下列兩種情況的時候使用multi link
,否則建議使用single link
搭配backlink
:
annotation可以幫助我們提供一些註記給object type
。EdgeDB預設有title
、description
及deprecated
三種。
考慮schema如下:
type User {
annotation deprecated := "BREAKING CHANGE! As of version x.y.z, the `User` object will be renamed to `USER`."
}
我們給予User object
一個deprecated annotation
。
此時可以利用introspect
可以檢視User object
的內部資訊:
select (introspect User) { annotations: {name, @value}};
{
schema::ObjectType {
annotations: {
schema::Annotation {
name: 'std::deprecated',
@value: 'BREAKING CHANGE! As of version x.y.z, the `User` object will be renamed to `USER`.',
},
},
},
}
除了預設的三種annotation
外,我們也可以自己定義。例如,這裡我們自己定義了一個hello annotation
:
abstract annotation hello;
type User {
annotation hello := "hello"
}
此時一樣可以利用introspect
來檢視User object
:
select (introspect User) { annotations: {name, @value}};
{
schema::ObjectType {
annotations: {
schema::Annotation {
name: 'default::hello',
@value: 'hello'
}
}
}
}
可以確認User object
確實註記有hello annotation
。
Trigger
就像一個callback,可以在object type
進行某一種操作後,接著執行另一個操作(但兩者會在同一個transaction內)。
rewrite
則是攔截insert
或update
等mutation
指令,並依據預先設定的expression
來改寫property
或link
後,再傳至database。
官方文件中提到這個例子:
type User {
required name: str;
trigger log_insert after insert for each do (
insert Log {
action := 'insert',
target_name := __new__.name
}
);
}
在每次insert
一個User object
同時,也會insert
一個Log object
。
此外,文件中也提到trigger
可能會引發callback地獄,需要小心使用。
官方文件中提到這個例子:
type Post {
required title: str;
required body: str;
modified: datetime {
rewrite insert, update using (datetime_of_statement())
}
}
在每一次進行insert
或update
時,EdgeDB會自動攔截並改寫query,將執行datetime_of_statement()
後的值指定給modified property
,再傳給database。
EdgeDB在下面三個地方,會自動幫大家進行index:
object
自動產生的id
(可以想成primary key)。link
(可以想成foreign key)。constraint exclusive
的property
。此外,也可以直接使用Postgres提供的index。